// Complete Reference Guide

The C Programming
Language

A comprehensive guide for developers already familiar with at least one programming language — covering syntax, semantics, memory model, and a full beginner project.

compiled statically typed procedural manual memory low-level access portable c11 / c17 / c23
§ 01 — Fundamentals

Introduction & Overview

C was created at Bell Labs by Dennis Ritchie in 1972 to write the Unix operating system. Nearly every modern programming language — Python, Java, C++, Swift, JavaScript, Rust — inherits its syntax and core concepts from C. If you already know any of these, C will feel simultaneously familiar and surprisingly manual.

What Makes C Different

Coming from a higher-level language, these are the biggest mental-model shifts:

ConceptHigher-level languagesC
MemoryGarbage-collected automaticallyYou allocate and free manually
StringsFirst-class String typeArrays of char ending in '\0'
Type safetyRuntime or compile errorsMany errors are silent undefined behavior
ArraysKnow their length (.length)Just a pointer — you track the size yourself
OOP / ClassesBuilt-inNot in C — use structs + function pointers
Exceptionsthrow / catchReturn codes or errno
ExecutionInterpreted or JIT-compiledCompiled directly to machine code

Hello, World!

hello.c
#include <stdio.h>   /* include the standard I/O header */

int main(void) {
    printf("Hello, World!\n");  /* \n = newline character */
    return 0;   /* return 0 to OS = success */
}
$ gcc -Wall -Wextra -std=c11 -o hello hello.c
$ ./hello
Hello, World!
Recommended flags-Wall -Wextra enable all compiler warnings — always use them during development. -std=c11 targets the C11 standard, a safe modern choice.

The Compilation Pipeline

StageToolInput → Output
PreprocessingcppSource .c → expanded source (macros expanded, includes inserted)
Compilationcc1Expanded source → assembly .s
AssemblyasAssembly → object file .o
LinkingldObject files + libraries → executable
§ 02

Data Types & Variables

Primitive Types

TypeSize (typical)RangeExample
char1 byte–128 to 127char c = 'A';
unsigned char1 byte0 to 255unsigned char b = 200;
short2 bytes–32,768 to 32,767short s = 1000;
int4 bytes–2,147,483,648 to 2,147,483,647int n = 42;
long4 or 8 bytesplatform-dependentlong x = 1000000L;
long long8 bytes±9.2 × 10¹⁸long long big = 9LL;
float4 bytes~±3.4 × 10³⁸float f = 3.14f;
double8 bytes~±1.8 × 10³⁰⁸double d = 3.14159;
_Bool / bool1 byte0 or 1bool ok = true; (needs <stdbool.h>)
voidNo value (used for functions returning nothing)
Fixed-width types (recommended)Include <stdint.h> and use int8_t, uint32_t, int64_t, etc. for sizes that are guaranteed regardless of platform.

Declaring & Initialising Variables

declarations
int age;                  /* declared but uninitialized — value is garbage! */
int age = 25;             /* declaration + initialization */
const double PI = 3.14159; /* const: cannot be changed */
char letter = 'A';         /* single quotes for char literals */
char name[] = "Alice";      /* char array — a C string */

/* Multiple declarations */
int x = 1, y = 2, z = 3;

Type Conversions

type conversion
/* Implicit — compiler converts automatically (may lose data) */
int    i = 3;
double d = i;     /* int → double: safe, no data loss */
int    n = 3.9;   /* double → int: truncates to 3, no warning! */

/* Explicit cast */
int a = 7, b = 2;
double result = (double)a / b;  /* 3.5 — without cast: 3 (integer division) */

Scope & Storage Classes

KeywordScopeLifetimePurpose
autoLocal blockBlock durationDefault for local vars (rarely written explicitly)
static (local)Local blockProgram lifetimePersists value between function calls
static (global)File scope onlyProgram lifetimeMakes a global variable/function file-private
externAny fileProgram lifetimeDeclares a variable defined in another .c file
registerLocal blockBlock durationHint to keep in CPU register (modern compilers ignore it)
§ 03

Operators

Arithmetic

arithmetic operators
int a = 10, b = 3;
a + b   // 13  — addition
a - b   // 7   — subtraction
a * b   // 30  — multiplication
a / b   // 3   — integer division (truncates!)
a % b   // 1   — modulo (remainder)
a++     // post-increment: returns a (10), then increments to 11
++a     // pre-increment:  increments first, then returns 11
a--     // post-decrement

Comparison & Logical

comparison & logical
a == b   // equal to        ← NOTE: = is assignment, == is comparison!
a != b   // not equal to
a <  b   // less than
a >  b   // greater than
a <= b   // less than or equal to
a >= b   // greater than or equal to

a && b   // logical AND — short-circuits (stops at first false)
a || b   // logical OR  — short-circuits (stops at first true)
!a      // logical NOT

Bitwise Operators

bitwise
int x = 0b1010;  /* 10 in binary (C23; or use 0x0A) */
int y = 0b1100;  /* 12 */

x &  y    // AND:        0b1000  = 8
x |  y    // OR:         0b1110  = 14
x ^  y    // XOR:        0b0110  = 6
~x        // NOT:        flips all bits
x << 2    // left-shift:  multiply by 4
x >> 1    // right-shift: divide by 2

/* Common bitwise patterns */
flags |=  FLAG;     // set a bit
flags &= ~FLAG;    // clear a bit
flags ^=  FLAG;     // toggle a bit
int on = flags & FLAG;  // test a bit

Assignment & Ternary

assignment & ternary
/* Compound assignment */
x += 3;    // x = x + 3
x -= 3;    // x = x - 3
x *= 2;    // x = x * 2
x /= 2;    // x = x / 2
x %= 3;    // x = x % 3
x <<= 1;  // x = x << 1

/* Ternary: condition ? value_if_true : value_if_false */
int max = (a > b) ? a : b;
const char *label = (score >= 60.0) ? "PASS" : "FAIL";

Operator Precedence (highest → lowest)

PrecedenceOperatorsAssociativity
1 (highest)() [] -> . ++ -- (postfix)Left → Right
2++ -- + - ! ~ * & (cast) sizeof (prefix/unary)Right → Left
3* / %Left → Right
4+ -Left → Right
5<< >>Left → Right
6< <= > >=Left → Right
7== !=Left → Right
8–10& ^ |Left → Right
11–12&& ||Left → Right
13?: (ternary)Right → Left
14= += -= *= /= %= …Right → Left
15 (lowest), (comma)Left → Right
💡
When in doubt, add parentheses. (a + b) * c is always clearer than relying on precedence rules.
§ 04

Control Flow

if / else if / else

conditionals
if (score >= 90) {
    printf("A\n");
} else if (score >= 80) {
    printf("B\n");
} else {
    printf("Below B\n");
}

/* Braces are optional for single statements, but ALWAYS use them */

switch

switch statement
switch (choice) {
    case 1:
        printf("One\n");
        break;    /* REQUIRED — without break, execution falls through! */
    case 2:
    case 3:           /* intentional fall-through: cases 2 and 3 share a body */
        printf("Two or three\n");
        break;
    default:          /* runs when no case matches */
        printf("Other\n");
}
/* switch works with: int, char, enum — NOT with strings or floats */

Loops

for loop
/* for (initializer; condition; update) { body } */
for (int i = 0; i < 10; i++) {
    printf("%d ", i);   /* prints: 0 1 2 3 4 5 6 7 8 9 */
}

/* Reverse iteration */
for (int i = 9; i >= 0; i--) { ... }

/* Any part can be omitted — infinite loop: */
for (;;) { ... }    /* equivalent to while(1) { ... } */
while loop
int n = 0;
while (n < 5) {
    printf("%d\n", n);
    n++;
}
/* condition is checked BEFORE each iteration; may run 0 times */
do-while loop
int input;
do {
    printf("Enter a positive number: ");
    scanf("%d", &input);
} while (input <= 0);
/* body runs AT LEAST ONCE, condition checked after — perfect for input loops */
break, continue, goto
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  /* skip to next iteration */
    if (i == 7) break;    /* exit loop immediately  */
    printf("%d ", i);      /* prints: 0 1 2 4 5 6   */
}

/* goto — use sparingly; mostly acceptable for error cleanup */
if (error) goto cleanup;
/* ... */
cleanup:
    free(buf);
    return -1;
§ 05

Functions

Syntax & Prototypes

function definition
/* return_type  function_name ( param_type param, ... ) { body } */

int add(int a, int b) {
    return a + b;
}

void greet(char *name) {    /* void = no return value */
    printf("Hello, %s!\n", name);
}

/* Prototypes at top of file (or in a .h header) */
int  add(int a, int b);
void greet(char *name);

Pass by Value vs Pass by Pointer

C is strictly pass-by-value. To let a function modify the caller's variable, pass its address (a pointer).

pass-by-value vs pass-by-pointer
/* Pass by value: caller's x is NOT modified */
void doubleIt_val(int x) { x *= 2; }

/* Pass by pointer: caller's x IS modified */
void doubleIt_ptr(int *x) { *x *= 2; }

int n = 5;
doubleIt_val(n);      /* n is still 5 */
doubleIt_ptr(&n);     /* n is now 10  — & gives the address */

Function Pointers

function pointers
/* Declare: return_type (*name)(param_types) */
int (*op)(int, int);

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }

op = add;  printf("%d\n", op(3,4));  /* 7  */
op = mul;  printf("%d\n", op(3,4));  /* 12 */

/* Use typedef for cleaner syntax */
typedef int (*BinaryOp)(int, int);
BinaryOp ops[] = { add, mul };  /* array of function pointers */

Variadic Functions

variadic (variable argument count)
#include <stdarg.h>

double average(int count, ...) {
    va_list args;
    va_start(args, count);
    double sum = 0;
    for (int i = 0; i < count; i++)
        sum += va_arg(args, double);
    va_end(args);
    return sum / count;
}
/* average(3, 1.0, 2.0, 3.0) == 2.0 */
§ 06 — Intermediate

Pointers

A pointer is a variable that holds a memory address. Pointers are the core mechanism behind dynamic memory, passing data by reference, and working with arrays and strings in C.

Pointer Basics

pointer fundamentals
int x = 42;
int *p = &x;   /* p holds x's address (& = address-of operator) */

printf("%d\n",  x);   /* 42    — value of x */
printf("%p\n",  p);   /* 0x7fff...  — address */
printf("%d\n", *p);   /* 42    — * dereferences: value at that address */

*p = 100;     /* write through pointer — x is now 100 */

/* Pointer arithmetic */
int arr[] = {10, 20, 30};
int *q = arr;        /* points to arr[0] */
printf("%d\n", *q);      /* 10 */
printf("%d\n", *(q+1));  /* 20 — advances by sizeof(int) bytes */
q++;                   /* q now points to arr[1] */
OperationSyntaxMeaning
Address-of&varGet the memory address of var
Dereference*ptrRead or write the value at the address ptr holds
Arrowptr->fieldDereference ptr and access struct field (= (*ptr).field)
Pointer arithmeticptr + nAddress + n × sizeof(*ptr) bytes
Pointer differencep2 - p1Number of elements between two pointers

Pointers to Pointers

double pointer
int x = 5;
int  *p  = &x;   /* pointer to int   */
int **pp = &p;   /* pointer to pointer to int */

printf("%d\n", **pp);  /* 5 — double dereference */

/* Used when a function needs to change what a pointer points to */
void reallocRoster(Student **roster, int newCap) {
    *roster = realloc(*roster, newCap * sizeof(Student));
}
Never dereference NULL or uninitialized pointers.Always initialise: int *p = NULL; and check if (p != NULL) before dereferencing. Dereferencing NULL causes a segmentation fault (crash).

const and Pointers

const pointer variations
int x = 10;
const int *p1 = &x;   /* pointer to const int: can't change *p1 */
int *const p2 = &x;   /* const pointer: can't change p2 itself */
const int *const p3 = &x; /* neither can change */

/* In function params, const signals "I won't modify your data" */
void printName(const char *name) { printf("%s\n", name); }
§ 07

Arrays & Strings

Arrays

arrays
/* Fixed-size arrays — size must be a compile-time constant */
int scores[5];                          /* uninitialized — garbage values */
int scores[5] = {90, 85, 78, 92, 88}; /* initialized */
int scores[] =  {90, 85, 78};          /* size inferred: 3 */
int zeros[100] = {0};                  /* all 100 elements set to 0 */

/* Access: zero-indexed */
scores[0] = 100;    /* first element */
scores[4] = 75;     /* last element — index = size - 1 */

/* Size of array (only works on the array itself, not a pointer to it) */
int n = sizeof(scores) / sizeof(scores[0]);  /* = 5 */

/* 2D array */
int matrix[3][4];           /* 3 rows, 4 columns */
matrix[1][2] = 42;          /* row 1, column 2 */
No bounds checking! Accessing scores[10] on a 5-element array is undefined behavior — the program may crash, produce wrong results, or silently corrupt memory. C never warns you about this at runtime.

Strings

C strings are char arrays terminated by a null character '\0'. There is no built-in String type.

c strings
char name[20] = "Alice";    /* A l i c e \0 ? ? ? ...  (20 bytes total) */
char *lit     = "Hello";    /* string literal — stored in read-only memory! */

/* Read string from user — fgets is safe, gets() is DANGEROUS (never use it) */
fgets(name, sizeof(name), stdin);        /* reads up to 19 chars + '\0' */
name[strcspn(name, "\n")] = '\0';    /* strip trailing newline from fgets */
FunctionHeaderDescription
strlen(s)<string.h>Length of s (not counting '\0')
strcpy(dst, src)<string.h>Copy src to dst — dst must be large enough!
strncpy(dst, src, n)<string.h>Copy at most n chars — safer
strcat(dst, src)<string.h>Append src onto dst
strcmp(a, b)<string.h>0=equal, <0 a<b, >0 a>b — never use ==
strstr(hay, needle)<string.h>Pointer to first occurrence of needle, or NULL
sprintf(buf, fmt, …)<stdio.h>Format into a buffer
snprintf(buf, n, fmt, …)<stdio.h>Safer: write at most n bytes
atoi(s)<stdlib.h>String to int (no error checking)
strtol(s, &end, base)<stdlib.h>String to long with error detection (prefer over atoi)
§ 08

Structs, Unions & Enums

struct — Grouping Data

structs
/* Define */
typedef struct {
    char   name[50];
    int    age;
    double salary;
} Employee;

/* Create and initialize */
Employee e1 = {"Alice", 30, 75000.0};
Employee e2 = {.name = "Bob", .age = 25};  /* designated initializer (C99+) */

/* Access fields with dot operator */
printf("%s is %d years old\n", e1.name, e1.age);

/* Access through a pointer with -> */
Employee *p = &e1;
printf("%s\n", p->name);   /* same as (*p).name */
p->salary = 80000.0;      /* modify through pointer */

/* Nested structs */
typedef struct {
    double x, y;
} Point;

typedef struct {
    Point  origin;
    double radius;
} Circle;

Circle c = {{0.0, 0.0}, 5.0};
printf("r=%.1f at (%.1f, %.1f)\n", c.radius, c.origin.x, c.origin.y);

enum — Named Constants

enum
typedef enum {
    NORTH = 0,
    SOUTH,   /* 1 — auto-increments */
    EAST,    /* 2 */
    WEST     /* 3 */
} Direction;

Direction dir = NORTH;
if (dir == NORTH) printf("Going north!\n");

/* Enums in switch — compiler warns if you miss a case */
switch (dir) {
    case NORTH: ... break;
    case SOUTH: ... break;
    default:    ... break;
}

union — Shared Memory

union
/* All members share the same memory address */
union Data {
    int   i;
    float f;
    char  bytes[4];
};
/* sizeof(union Data) == sizeof(largest member) == 4 */

union Data d;
d.i = 42;          /* write as int */
/* now d.f and d.bytes are also "valid" but their interpretation changes */
/* Only the last-written member should be read */
§ 09

Dynamic Memory Management

Dynamic memory is allocated at runtime on the heap — it persists until you explicitly free it. Functions are in <stdlib.h>.

FunctionInitialises?Description
malloc(size)No — garbageAllocate size bytes
calloc(n, size)Yes — all zerosAllocate n × size bytes, zeroed
realloc(ptr, size)Preserves old dataResize block; may move to new address
free(ptr)Release block back to OS (must call exactly once)
malloc / realloc / free pattern
#include <stdlib.h>

/* Allocate an array of 10 ints */
int *arr = malloc(10 * sizeof(int));

if (arr == NULL) {                  /* ALWAYS check for NULL */
    fprintf(stderr, "Out of memory!\n");
    return 1;
}

for (int i = 0; i < 10; i++) arr[i] = i * i;

/* Grow the array — CRITICAL: use a temp pointer */
int *tmp = realloc(arr, 20 * sizeof(int));
if (tmp == NULL) {
    /* realloc failed — arr is still valid, handle error */
    free(arr);
    return 1;
}
arr = tmp;  /* safe to update now */

free(arr);    /* release when done */
arr = NULL;   /* good practice: prevent use-after-free */

Common Memory Bugs

BugCauseTool to Detect
Memory leakmalloc without a matching freeValgrind, AddressSanitizer
Use-after-freeAccessing memory after calling free()AddressSanitizer
Double freeCalling free() twice on same pointerAddressSanitizer
Buffer overflowWriting past the end of an allocated blockAddressSanitizer, Valgrind
Dangling pointerPointer to freed memorySet to NULL after free
💡
Compile with sanitizers during development:
gcc -fsanitize=address,undefined -g -o prog prog.c
These catch memory errors at runtime with helpful messages.
§ 10

File I/O

file read & write
#include <stdio.h>

/* ── Writing ───────────────────────────────────────────── */
FILE *fp = fopen("data.txt", "w");   /* "w" = create or overwrite */
if (fp == NULL) { perror("fopen"); return 1; }

fprintf(fp, "Value: %d\n", 42);      /* like printf, but to fp */
fputs("Hello file\n", fp);
fclose(fp);                           /* ALWAYS close the file */

/* ── Reading ───────────────────────────────────────────── */
fp = fopen("data.txt", "r");
if (fp == NULL) { perror("fopen"); return 1; }

char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
    printf("%s", line);            /* fgets keeps the newline */
}
fclose(fp);

/* ── Binary I/O ─────────────────────────────────────────── */
fp = fopen("data.bin", "wb");
int nums[] = {1, 2, 3, 4};
fwrite(nums, sizeof(int), 4, fp);   /* write 4 ints */
fclose(fp);

fp = fopen("data.bin", "rb");
int read_back[4];
fread(read_back, sizeof(int), 4, fp); /* read 4 ints */
fclose(fp);
ModeOpens forFile exists?No file?
"r"ReadOpens OKFails (NULL)
"w"WriteTruncates!Creates
"a"AppendAppends to endCreates
"r+"Read + WriteOpens OKFails (NULL)
"w+"Read + WriteTruncates!Creates
"rb" / "wb"Binary read/write
§ 11

The Preprocessor

preprocessor directives
/* ── Includes ──────────────────────────────────────────── */
#include <stdio.h>         /* system header */
#include "myheader.h"       /* local header  */

/* ── Constants ─────────────────────────────────────────── */
#define PI          3.14159265
#define MAX_BUF     1024
#define SQUARE(x)   ((x) * (x))   /* macro function — parens protect operands */

/* ── Conditional compilation ────────────────────────────── */
#ifdef DEBUG
    printf("debug: x = %d\n", x);
#endif

#if defined(_WIN32)
    /* Windows-specific code */
#elif defined(__linux__)
    /* Linux-specific code */
#endif

/* ── Include guard (every .h file should have this) ─────── */
#ifndef MYHEADER_H
#define MYHEADER_H
    /* header contents */
#endif

/* Modern alternative: */
#pragma once    /* supported by all major compilers */
Macro pitfallsThe macro SQUARE(x) with parentheses is critical. Without them, SQUARE(a+b) would expand to a+b * a+b — wrong! Always wrap macro parameters and the whole expression in parentheses.
§ 12

Standard Library Reference

printf / scanf Format Specifiers

SpecifierTypeExample
%d / %iintprintf("%d", 42)42
%uunsigned intprintf("%u", 42u)42
%ldlongprintf("%ld", 1000000L)
%lldlong longprintf("%lld", 9LL)
%ffloat / double (printf)printf("%f", 3.14)3.140000
%lfdouble (scanf only)scanf("%lf", &d)
%.2fdouble, 2 decimal placesprintf("%.2f", 3.14159)3.14
%eScientific notationprintf("%e", 3.14)3.140000e+00
%ccharprintf("%c", 'A')A
%schar* stringprintf("%s", "hi")hi
%pvoid* pointerprintf("%p", ptr)0x7ffe...
%x / %Xunsigned hexprintf("%x", 255)ff
%ounsigned octalprintf("%o", 8)10
%zusize_tprintf("%zu", sizeof(int))
%%Literal %printf("100%%")100%

Width, Padding & Alignment

format widths
printf("%10d",  42);   /* right-aligned:  '        42' */
printf("%-10d", 42);   /* left-aligned:   '42        ' */
printf("%010d", 42);   /* zero-padded:    '0000000042' */
printf("%+d",   42);   /* always show sign: '+42'      */
printf("%6.2f", 3.14); /* width 6, 2 decimal:  '  3.14' */

Key Header Files

HeaderKey Contents
<stdio.h>printf, scanf, fopen, fclose, fgets, fprintf, sprintf, snprintf, perror
<stdlib.h>malloc, calloc, realloc, free, exit, atoi, strtol, rand, srand, qsort, bsearch
<string.h>strlen, strcpy, strncpy, strcmp, strcat, strstr, strchr, memcpy, memset
<math.h>sqrt, pow, fabs, sin, cos, log, ceil, floor — link with -lm
<ctype.h>isdigit, isalpha, isupper, islower, tolower, toupper, isspace
<stdint.h>int8_t, uint8_t, int16_t, uint32_t, int64_t, etc.
<stdbool.h>bool, true, false (C99+)
<assert.h>assert(expr) — aborts with message if expr is false
<errno.h>errno — error code set by library calls
<time.h>time, clock, difftime, struct tm, strftime
<limits.h>INT_MAX, INT_MIN, CHAR_MAX, LONG_MAX, etc.
§ 13 — Beginner Project

Student Grade Tracker

Beyond Hello World: A Real Program

This single-file program is specifically designed to exercise the most important features of C in a real, working context. It's a command-line student grade manager — practical enough to be useful, simple enough to understand completely.

#define macros typedef enum typedef struct Function prototypes Pointers & -> Arrays & strings for / while / do-while switch / if-else malloc / realloc / free File I/O printf format strings Ternary operator

What the Program Does

Menu OptionC Concepts Used
1. Add studentfgets, strncpy, scanf with validation, realloc to grow array
2. View allfor loop, pointer access, printf field widths, ternary
3. Search by namestrcmp, boolean flag, for loop with early exit
4. Save to filefopen("w"), fprintf, fclose
5. Load from filefopen("r"), fgets, fscanf, realloc
6. Statisticsaccumulator pattern, min/max, cast, modulo, pass rate
7. Quitflag variable, free()
$ gcc -Wall -Wextra -std=c11 -o tracker student_tracker.c
$ ./tracker
§ 14

Full Annotated Source

Part 1 — Headers, Macros, Types, and Prototypes

student_tracker.c — part 1 of 5
/*
 * CONCEPT 1 — #include & #define (Preprocessor)
 * These run BEFORE compilation. #include inserts a header file.
 * #define creates a constant via text substitution (no type checking).
 */
#include <stdio.h>    /* printf, scanf, fopen, fclose, FILE */
#include <stdlib.h>   /* malloc, realloc, free, exit        */
#include <string.h>   /* strcpy, strcmp, strlen, strcspn    */
#include <ctype.h>    /* toupper (converts char to upper)   */

#define MAX_NAME      64
#define SAVE_FILE     "students.dat"
#define PASSING_SCORE 60.0

/*
 * CONCEPT 2 — enum (Named Integer Constants)
 * Assigns readable names to integers. ADD=1, VIEW=2, etc.
 * Use enums instead of bare magic numbers.
 * typedef lets us write 'MenuChoice' instead of 'enum MenuChoice'.
 */
typedef enum {
    ADD    = 1, VIEW = 2, SEARCH = 3,
    SAVE   = 4, LOAD = 5, STATS  = 6, QUIT = 7
} MenuChoice;

/*
 * CONCEPT 3 — struct (Custom Composite Data Type)
 * Groups related fields into one named type.
 * char name[MAX_NAME] is a fixed-length string (char array).
 */
typedef struct {
    char   name[MAX_NAME];  /* fixed-size char array = C string */
    int    id;
    double score;
    char   grade;
} Student;

/*
 * CONCEPT 4 — Function Prototypes
 * In C, a function must be declared before it is used.
 * These prototypes tell the compiler the signature; the full
 * definition appears after main().
 *
 * 'const Student *s' = pointer to read-only Student (we won't modify it)
 * 'Student **roster' = pointer-to-pointer (needed when function may
 *                      change what the pointer itself points to)
 */
char calculateGrade(double score);
void printStudent(const Student *s);
void addStudent(Student **roster, int *count, int *capacity);
void viewAll(const Student *roster, int count);
void searchByName(const Student *roster, int count);
void showStats(const Student *roster, int count);
void saveToFile(const Student *roster, int count);
void loadFromFile(Student **roster, int *count, int *capacity);
void printMenu(void);
void clearInputBuffer(void);

Part 2 — main() and the Menu Loop

student_tracker.c — part 2 of 5
/*
 * CONCEPT 9 — Dynamic Memory
 * malloc(n * sizeof(T)) allocates n×sizeof(T) bytes on the heap.
 * Returns void* (raw pointer) or NULL on failure.
 * We start with capacity=4 and grow with realloc() when full.
 */
int main(void) {
    int      capacity = 4;
    int      count    = 0;
    Student *roster   = malloc(capacity * sizeof(Student));

    if (roster == NULL) {   /* ALWAYS check malloc's return */
        fprintf(stderr, "Fatal: out of memory\n");
        return 1;
    }

    printf("=== C Language Tour: Grade Tracker ===\n\n");

    /*
     * CONCEPT 8 — do-while loop
     * Executes the body AT LEAST ONCE, then checks the condition.
     * Perfect for menu loops where you always show the menu first.
     */
    int running = 1;
    do {
        printMenu();

        /*
         * scanf("%d", &choice) reads one integer.
         * The & gives scanf the ADDRESS of 'choice' so it can
         * write into it. Without &, scanf would get a copy.
         * scanf returns the number of items successfully read.
         */
        int choice;
        if (scanf("%d", &choice) != 1) {
            clearInputBuffer();
            printf("  Invalid input.\n\n");
            continue;
        }
        clearInputBuffer();

        /*
         * CONCEPT 8 — switch
         * Jumps directly to the matching case.
         * 'break' is REQUIRED — without it execution falls through
         * to the next case (usually a bug!).
         */
        switch (choice) {
            case ADD:    addStudent(&roster, &count, &capacity); break;
            case VIEW:   viewAll(roster, count);                 break;
            case SEARCH: searchByName(roster, count);            break;
            case SAVE:   saveToFile(roster, count);             break;
            case LOAD:   loadFromFile(&roster, &count, &capacity); break;
            case STATS:  showStats(roster, count);              break;
            case QUIT:
                printf("  Goodbye!\n\n");
                running = 0;
                break;
            default:
                printf("  Unknown option.\n\n");
        }
    } while (running);

    free(roster);    /* every malloc needs a matching free */
    roster = NULL;   /* prevent use-after-free */
    return 0;
}

Part 3 — Adding & Displaying Students

student_tracker.c — part 3 of 5
/*
 * calculateGrade — if/else chain, comparison operators
 */
char calculateGrade(double score) {
    if      (score >= 90.0) return 'A';
    else if (score >= 80.0) return 'B';
    else if (score >= 70.0) return 'C';
    else if (score >= 60.0) return 'D';
    else                    return 'F';
}

/*
 * printStudent — pointer parameter, -> operator, ternary
 *
 * 'const Student *s': we CAN'T modify *s through this pointer.
 * s->name  is shorthand for  (*s).name
 * The ternary (score >= PASSING ? "PASS" : "FAIL") picks a string.
 */
void printStudent(const Student *s) {
    printf("  %-20s  ID:%04d  Score:%6.2f  Grade:%c  [%s]\n",
           s->name, s->id, s->score, s->grade,
           s->score >= PASSING_SCORE ? "PASS" : "FAIL");
}

/*
 * addStudent — realloc, fgets, input validation with while loop
 *
 * Takes Student **roster because the function may need to change
 * what roster POINTS TO after a realloc (which can move the block).
 * A pointer-to-pointer lets us update the caller's pointer.
 */
void addStudent(Student **roster, int *count, int *capacity) {
    if (*count >= *capacity) {
        int     newCap = *capacity * 2;
        /* CRITICAL: use a temp pointer — if realloc fails,
         * the original pointer is still valid.
         * Writing  *roster = realloc(*roster, ...)  would
         * overwrite the pointer on failure → memory leak!     */
        Student *tmp = realloc(*roster, newCap * sizeof(Student));
        if (tmp == NULL) { printf("  Memory error.\n"); return; }
        *roster   = tmp;
        *capacity = newCap;
        printf("  (Array grown to capacity %d)\n", newCap);
    }

    Student *s = &(*roster)[*count];  /* pointer to next empty slot */

    printf("\n  Enter student name: ");
    /* fgets(buf, size, stdin): reads up to size-1 chars safely.
     * NEVER use gets() — no bounds checking, buffer overflow risk! */
    if (fgets(s->name, MAX_NAME, stdin) == NULL) return;
    s->name[strcspn(s->name, "\n")] = '\0';  /* strip trailing \n */

    if (strlen(s->name) == 0) { printf("  Name cannot be empty.\n\n"); return; }

    s->id = *count + 1001;   /* auto-assign ID */

    /* Input validation loop — keep asking until valid */
    while (1) {
        printf("  Enter score (0-100): ");
        if (scanf("%lf", &s->score) == 1
                && s->score >= 0.0 && s->score <= 100.0) {
            clearInputBuffer();
            break;
        }
        clearInputBuffer();
        printf("  Invalid. Enter a number 0-100.\n");
    }

    s->grade = calculateGrade(s->score);
    (*count)++;

    printf("\n  Added: ");
    printStudent(s);
    printf("\n");
}

Part 4 — View, Search, and Statistics

student_tracker.c — part 4 of 5
/*
 * viewAll — for loop, array-as-pointer, guard clause
 */
void viewAll(const Student *roster, int count) {
    if (count == 0) { printf("\n  No students yet.\n\n"); return; }
    printf("\n  %-20s  %-8s  %-10s  %-7s  %s\n",
           "Name", "ID", "Score", "Grade", "Result");
    printf("  %s\n", "------------------------------------------------------");
    /* for loop: init; condition; update */
    for (int i = 0; i < count; i++) {
        printStudent(&roster[i]);   /* &roster[i] = pointer to element i */
    }
    printf("\n");
}

/*
 * searchByName — strcmp, boolean flag variable
 * NOTE: We CANNOT use == to compare C strings!
 * == compares pointer addresses, not string contents.
 * strcmp(a, b) returns 0 when strings are equal.
 */
void searchByName(const Student *roster, int count) {
    if (count == 0) { printf("\n  No students.\n\n"); return; }

    char query[MAX_NAME];
    printf("\n  Enter name to search: ");
    if (fgets(query, MAX_NAME, stdin) == NULL) return;
    query[strcspn(query, "\n")] = '\0';

    int found = 0;   /* boolean flag: 0=false, 1=true */
    for (int i = 0; i < count; i++) {
        if (strcmp(roster[i].name, query) == 0) {
            if (!found) printf("\n  Results:\n");
            printStudent(&roster[i]);
            found = 1;
        }
    }
    if (!found) printf("\n  No student named \"%s\".\n", query);
    printf("\n");
}

/*
 * showStats — accumulator pattern, min/max, cast, ternary
 *
 * (double)pass / count * 100.0
 *   The cast (double)pass converts int→double BEFORE division,
 *   preventing integer division which would give 0 for pass < count.
 */
void showStats(const Student *roster, int count) {
    if (count == 0) { printf("\n  No students.\n\n"); return; }

    double sum  = 0.0, high = roster[0].score, low = roster[0].score;
    int    pass = 0,  fail = 0;

    for (int i = 0; i < count; i++) {
        sum += roster[i].score;
        if (roster[i].score > high) high = roster[i].score;
        if (roster[i].score < low)  low  = roster[i].score;
        /* ternary increments one of two counters */
        (roster[i].score >= PASSING_SCORE) ? pass++ : fail++;
    }

    printf("\n  Students  : %d\n",  count);
    printf("  Average   : %.2f\n", sum / count);
    printf("  Highest   : %.2f\n", high);
    printf("  Lowest    : %.2f\n", low);
    printf("  Pass Rate : %.1f%%\n\n",
           count > 0 ? (double)pass / count * 100.0 : 0.0);
}

Part 5 — File I/O and Utilities

student_tracker.c — part 5 of 5
/*
 * saveToFile — FILE*, fopen("w"), fprintf, fclose
 * fopen returns FILE* or NULL. Always check.
 * fprintf works like printf but writes to fp.
 * fclose flushes the buffer and closes the file descriptor.
 */
void saveToFile(const Student *roster, int count) {
    FILE *fp = fopen(SAVE_FILE, "w");
    if (fp == NULL) { perror("fopen"); return; }

    fprintf(fp, "%d\n", count);
    for (int i = 0; i < count; i++)
        fprintf(fp, "%s\n%d\n%.2f\n%c\n",
                roster[i].name, roster[i].id,
                roster[i].score, roster[i].grade);

    fclose(fp);
    printf("\n  Saved %d student(s) to \"%s\".\n\n", count, SAVE_FILE);
}

/*
 * loadFromFile — fopen("r"), fscanf, fgets, realloc
 */
void loadFromFile(Student **roster, int *count, int *capacity) {
    FILE *fp = fopen(SAVE_FILE, "r");
    if (fp == NULL) { printf("\n  No save file found.\n\n"); return; }

    int newCount;
    if (fscanf(fp, "%d\n", &newCount) != 1) {
        printf("\n  Corrupt save file.\n\n");
        fclose(fp); return;
    }

    Student *tmp = realloc(*roster, newCount * sizeof(Student));
    if (tmp == NULL && newCount > 0) {
        printf("\n  Memory error.\n\n");
        fclose(fp); return;
    }
    *roster = tmp; *capacity = newCount;

    for (int i = 0; i < newCount; i++) {
        if (fgets((*roster)[i].name, MAX_NAME, fp) == NULL) break;
        (*roster)[i].name[strcspn((*roster)[i].name, "\n")] = '\0';
        fscanf(fp, "%d\n",   &(*roster)[i].id);
        fscanf(fp, "%lf\n", &(*roster)[i].score);
        fscanf(fp, " %c\n", &(*roster)[i].grade);  /* space skips whitespace */
    }
    fclose(fp);
    *count = newCount;
    printf("\n  Loaded %d student(s) from \"%s\".\n\n", newCount, SAVE_FILE);
}

void printMenu(void) {
    printf("--- MENU ---\n");
    printf("%d. Add  %d. View  %d. Search  %d. Save  %d. Load  %d. Stats  %d. Quit\n",
           ADD, VIEW, SEARCH, SAVE, LOAD, STATS, QUIT);
    printf("Choice: ");
}

/*
 * clearInputBuffer — consume leftover chars in stdin
 * After scanf reads a number, '\n' stays in the buffer.
 * The next fgets would read that '\n' and return immediately.
 * This loop discards everything up to and including '\n'.
 */
void clearInputBuffer(void) {
    int c;
    while ((c = getchar()) != '\n' && c != EOF)
        ;   /* empty body — work is done in the condition */
}
§ 15

Exercises & Next Steps

Beginner Exercises (build on the tracker)

#ExerciseConcepts Practiced
1Add a "Delete student by ID" menu option — shift array elements downarrays, loops, memmove
2Add a second exam score field and compute a weighted averagestructs, arithmetic
3Sort the roster by score using qsort() from <stdlib.h>function pointers, stdlib
4Add an "Edit" option: find student by ID, update score, recalculate gradeloops, structs, pointer
5Change char name[MAX_NAME] to a dynamically allocated char *malloc, free, pointers

Intermediate Projects

ProjectNew Concepts
Linked list (nodes with next pointer)struct self-reference, malloc/free per node
Stack and queue using arraysdata structures, function design
Simple text adventure gameenums, switch, multi-file (header + source)
Word frequency counter (file → hash table)file I/O, strings, dynamic memory, qsort
Calculator with operator precedenceparsing, stacks, function pointers

Best Practices Checklist

RuleWhy
Always initialise variablesUninitialized values are garbage — undefined behavior
Check return values of malloc, fopen, scanfThey can fail; ignoring failures causes crashes later
Use const for read-only parametersDocuments intent, enables compiler optimizations
Prefer snprintf over sprintfPrevents buffer overflows
Match every malloc with exactly one freePrevents memory leaks and double-free bugs
Compile with -Wall -Wextra and fix every warningCatches bugs before they become crashes
Use size_t for sizes and countsIt's unsigned and the right width on every platform
Never use gets()No bounds checking — unconditional buffer overflow risk
Set pointer to NULL after free()Prevents accidental use-after-free
Put declarations in .h, definitions in .cStandard project structure for multi-file programs
🚀
The fastest way to learn C: Write code. Break things. Compile with -fsanitize=address, read the error, fix it. Repeat. The compiler and the sanitizers are your best teachers.
// End of Guide
The best C programmer is one who never stops reading compiler warnings.